/* Copyright (C) 2000,2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY 
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion.procedural;

import artofillusion.*;
import artofillusion.math.*;
import buoy.widget.*;
import java.awt.*;
import java.io.*;
import java.lang.reflect.*;

/** This represents a module in a procedure.  This is an abstract class, whose subclasses
    represent specific kinds of modules. */

public class Module
{
  protected IOPort input[], output[];
  public Module linkFrom[];
  public int linkFromIndex[];
  protected String name;
  protected Rectangle bounds;
  protected boolean checked;
  
  protected static final Font defaultFont = Font.decode("Serif");
  protected static final FontMetrics defaultMetrics = Toolkit.getDefaultToolkit().getFontMetrics(defaultFont);

  public Module(String name, IOPort input[], IOPort output[], Point position)
  {
    this.name = name;
    this.input = input;
    this.output = output;
    linkFrom = new Module [input.length];
    linkFromIndex = new int [input.length];
    for (int i = 0; i < input.length; i++)
      input[i].setModule(this);
    for (int i = 0; i < output.length; i++)
      output[i].setModule(this);
    bounds = new Rectangle(position.x, position.y, 0, 0);
    layout();
  }
  
  /** Get the name of this module. */
  
  public String getName()
  {
    return name;
  }
  
  /** Get the boundary rectangle for this module. */
  
  public Rectangle getBounds()
  {
    return bounds;
  }
  
  /** Move this module to a new location. */
  
  public void setPosition(int x, int y)
  {
    bounds.x = x;
    bounds.y = y;
    layout();
  }
  
  /** Get a list of the input ports for this module. */
  
  public IOPort [] getInputPorts()
  {
    return input;
  }
  
  /** Get a list of the output ports for this module. */
  
  public IOPort [] getOutputPorts()
  {
    return output;
  }
  
  /** Get the index of a particular input port. */
  
  public int getInputIndex(IOPort port)
  {
    for (int i = 0; i < input.length; i++)
      if (input[i] == port)
        return i;
    return -1;
  }
  
  /** Get the index of a particular output port. */
  
  public int getOutputIndex(IOPort port)
  {
    for (int i = 0; i < output.length; i++)
      if (output[i] == port)
        return i;
    return -1;
  }
  
  /** Determine whether an input port is connected to anything. */
  
  public boolean inputConnected(int which)
  {
    return (linkFrom[which] != null);
  }
  
  /** Determine whether the specified point is over an IOPort, and if so, return the port. */
  
  public IOPort getClickedPort(Point pos)
  {
    for (int i = 0; i < input.length; i++)
      if (input[i].contains(pos))
        return input[i];
    for (int i = 0; i < output.length; i++)
      if (output[i].contains(pos))
        return output[i];
    return null;
  }
  
  /** Specify the module and port which one of the input ports is connected to. */
  
  public void setInput(IOPort which, IOPort port)
  {
    for (int i = 0; i < input.length; i++)
      if (input[i] == which)
        {
          if (port == null)
            {
              linkFrom[i] = null;
              return;
            }
          Module module = port.getModule();
          for (int j = 0; j < module.output.length; j++)
            if (module.output[j] == port)
              {
                linkFrom[i] = module;
                linkFromIndex[i] = j;
              }
        }
  }
  
  /** Calculate the size on the screen of this module.  The default implementation makes it
      large enough to display the name of the module. */
  
  public void calcSize()
  {
    bounds.width = defaultMetrics.stringWidth(name)+IOPort.SIZE*4;
    bounds.height = defaultMetrics.getMaxAscent()+defaultMetrics.getMaxDescent()+IOPort.SIZE*4;
    
    // Depending on how many ports there are, we might need to make it larger.
    
    int numtop = 0, numbottom = 0, numleft = 0, i, j;    
    for (i = 0; i < input.length; i++)
      {
        j = input[i].getLocation();
        if (j == IOPort.TOP)
          numtop++;
        else if (j == IOPort.BOTTOM)
          numbottom++;
        else
          numleft++;
      }
    if (Math.max(numtop, numbottom)*IOPort.SIZE*4 > bounds.width)
      bounds.width = Math.max(numtop, numbottom)*IOPort.SIZE*4;
    if (Math.max(numleft, output.length)*IOPort.SIZE*4 > bounds.height)
      bounds.height = Math.max(numleft, output.length)*IOPort.SIZE*4;
  }
  
  /** Layout the module's onscreen representation.  This should be called any time the
      module is moved or resized. */
  
  public void layout()
  {
    calcSize();
    int numtop = 0, numbottom = 0, numleft = 0;
    int top = 0, bottom = 0, left = 0, i, j;
    
    for (i = 0; i < input.length; i++)
      {
        j = input[i].getLocation();
        if (j == IOPort.TOP)
          numtop++;
        else if (j == IOPort.BOTTOM)
          numbottom++;
        else
          numleft++;
      }
    for (i = 0; i < input.length; i++)
      {
        j = input[i].getLocation();
        if (j == IOPort.TOP)
          input[i].setPosition(bounds.x+(bounds.width*(++top))/(numtop+1), bounds.y);
        else if (j == IOPort.BOTTOM)
          input[i].setPosition(bounds.x+(bounds.width*(++bottom))/(numbottom+1), bounds.y+bounds.height);
        else
          input[i].setPosition(bounds.x, bounds.y+(bounds.height*(++left))/(numleft+1));
      }
    for (i = 0; i < output.length; i++)
      output[i].setPosition(bounds.x+bounds.width+IOPort.SIZE, bounds.y+(bounds.height*(i+1))/(output.length+1));
  }
  
  /** Draw the module on the screen.  This draws the outline and the ports, then calls
      drawContents() to draw the contents. */
  
  public void draw(Graphics2D g)
  {
    g.setColor(Color.lightGray);
    g.fill3DRect(bounds.x, bounds.y, bounds.width, bounds.height, true);
    for (int i = 0; i < input.length; i++)
      input[i].draw(g);
    for (int i = 0; i < output.length; i++)
      output[i].draw(g);
    drawContents(g);
  }
  
  /** Draw the contents of the module.  The default implementation simply draws the name. */
  
  protected void drawContents(Graphics2D g)
  {
    g.setColor(Color.black);
    g.setFont(defaultFont);
    g.drawString(name, bounds.x+(bounds.width-defaultMetrics.stringWidth(name))/2, 
	bounds.y+(bounds.height/2)+(defaultMetrics.getAscent()/2));
  }
  
  /** This method is used to check feedback loops in a procedure. */
  
  public boolean checkFeedback()
  {
    if (checked)
      return true;
    checked = true;
    for (int i = 0; i < linkFrom.length; i++)
      if (linkFrom[i] != null)
        if (linkFrom[i].checkFeedback())
          return true;
    checked = false;
    return false;
  }
  
  /** This should display a user interface for editing the module, and return true if the
      module is changed.  The default implementation does nothing. */
  
  public boolean edit(BFrame fr, Scene theScene)
  {
    return false;
  }
  
  /** This method initializes the module in preparation for evaluating the procedure at a
      new point.  The default implementation does nothing.  Subclasses whose output depends
      on the point should override this method. */
  
  public void init(PointInfo p)
  {
  }
  
  /** Get the average value of the specified output port.  If the specified output port
      does not have a value type of NUMBER, the result is undefined.  Blur specifies the
      amount of smoothing to use.  Subclasses which can return values should override this 
      method. */
  
  public double getAverageValue(int which, double blur)
  {
    return 0.0;
  }

  /** Get the uncertainty in the value of the specified output port.  If the specified 
      output port does not have a value type of NUMBER, the result is undefined.  Blur
      specifies the amount of smoothing to use.  Subclasses which can return values should 
      override this method. */
  
  public double getValueError(int which, double blur)
  {
    return 0.0;
  }

  /** Get the gradient of the value of the specified output port.  If the specified 
      output port does not have a value type of NUMBER, the result is undefined.  Blur
      specifies the amount of smoothing to use.  Subclasses which can return values should 
      override this method. */
  
  public void getValueGradient(int which, Vec3 grad, double blur)
  {
    grad.set(0.0, 0.0, 0.0);
  }

  /** Get the color of the specified output port.  If the specified output port
      does not have a value type of COLOR, the result is undefined.  Blur specifies the
      amount of smoothing to use.  Subclasses which can return colors should override this 
      method. */
  
  public void getColor(int which, RGBColor color, double blur)
  {
  }
  
  /** Create a duplicate of this module.  Subclasses with adjustable parameters should
      override this. */
  
  public Module duplicate()
  {
    try
    {
      Constructor con = getClass().getConstructor(new Class [] {Point.class});
      return (Module) con.newInstance(new Object [] {new Point(bounds.x, bounds.y)});
    }
    catch (Exception ex)
    {
      return null;
    }
  }
  
  /** Write out the module's parameters to an output stream.  Subclasses with editable
     parameters should override this method. */
  
  public void writeToStream(DataOutputStream out, Scene theScene) throws IOException
  {
  }
  
  /** Read in the module's parameters from an input stream.  Subclasses with editable
      parameters should override this method. */
  
  public void readFromStream(DataInputStream out, Scene theScene) throws IOException
  {
  }
}
